import { EQEnvInfo, EQEnvKind } from ".";
import { OUTPUT_MARKER_SCRIPT } from "../../../common/process/internal/scripts";
import { IDisposableRegistry } from "../../../common/types";
import { createDeferred, Deferred } from "../../../common/utils/async";
import { createRunningWorkerPool, IWorkerPool, QueuePosition } from "../../../common/utils/workerPool";
import { traceError, traceInfo } from "../../../logging";
import { Conda, CONDA_ACTIVATION_TIMEOUT } from "../../common/environmentManagers/conda";
import { normCasePath } from "../../common/externalDependencies";
import { buildEQExecInfo } from "../../exec";
import { getInterpreterInfo, InterpreterInformation } from "./interpreter";

export enum EnvironmentInfoServiceQueuePriority {
    Default,
    High,
}

export interface IEnvironmentInfoService {
    getEnvironmentInfo(
        env: EQEnvInfo,
        priority?: EnvironmentInfoServiceQueuePriority,
    ): Promise<InterpreterInformation | undefined>;
}

async function buildEnvironmentInfo(env: EQEnvInfo): Promise<InterpreterInformation | undefined> {
    const eq = [env.executable.filename, OUTPUT_MARKER_SCRIPT];
    const interpreterInfo = await getInterpreterInfo(buildEQExecInfo(eq, undefined, env.executable.filename));
    return interpreterInfo;
}

class EnvironmentInfoService implements IEnvironmentInfoService {
    private readonly cache: Map<string, Deferred<InterpreterInformation>> = new Map<
        string,
        Deferred<InterpreterInformation>
    >();

    private workerPool?: IWorkerPool<EQEnvInfo, InterpreterInformation | undefined>;

    private condaRunWorkerPool?: IWorkerPool<EQEnvInfo, InterpreterInformation | undefined>;

    public dispose(): void {
        if (this.workerPool !== undefined) {
            this.workerPool.stop();
            this.workerPool = undefined;
        }
        if (this.condaRunWorkerPool !== undefined) {
            this.condaRunWorkerPool.stop();
            this.condaRunWorkerPool = undefined;
        }
    }

    public async getEnvironmentInfo(
        env: EQEnvInfo,
        priority?: EnvironmentInfoServiceQueuePriority,
    ): Promise<InterpreterInformation | undefined> {
        const interpreterPath = env.executable.filename;
        const result = this.cache.get(normCasePath(interpreterPath));
        if (result !== undefined) {
            // Another call for this environment has already been made, return its result.
            return result.promise;
        }

        const deferred = createDeferred<InterpreterInformation>();
        this.cache.set(normCasePath(interpreterPath), deferred);
        this._getEnvironmentInfo(env, priority)
            .then((r) => {
                deferred.resolve(r);
            })
            .catch((ex) => {
                deferred.reject(ex);
            });
        return deferred.promise;
    }

    public async _getEnvironmentInfo(
        env: EQEnvInfo,
        priority?: EnvironmentInfoServiceQueuePriority,
    ): Promise<InterpreterInformation | undefined> {
        if (this.workerPool === undefined) {
            this.workerPool = createRunningWorkerPool<EQEnvInfo, InterpreterInformation | undefined>(
                buildEnvironmentInfo,
            );
        }

        let reason: unknown;
        let r = await addToQueue(this.workerPool, env, priority).catch((err) => {
            reason = err;
            return undefined;
        });

        if (r === undefined) {
            // Even though env kind is not conda, it can still be a conda environment
            // as complete env info may not be available at this time.
            const isCondaEnv = env.kind === EQEnvKind.Conda;
            if (isCondaEnv) {
                traceInfo(
                    `Validating ${env.executable.filename} normally failed with error, falling back to using conda run: (${reason})`,
                );
                if (this.condaRunWorkerPool === undefined) {
                    // Create a separate queue for validation using conda, so getting environment info for
                    // other types of environment aren't blocked on conda.
                    this.condaRunWorkerPool = createRunningWorkerPool<
                        EQEnvInfo,
                        InterpreterInformation | undefined
                    >(buildEnvironmentInfoUsingCondaRun);
                }
                r = await addToQueue(this.condaRunWorkerPool, env, priority).catch((err) => {
                    traceError(err);
                    return undefined;
                });
            } else if (reason) {
                traceError(reason);
            }
        }
        return r;
    }
}
let envInfoService: IEnvironmentInfoService | undefined;
export function getEnvironmentInfoService(disposables?: IDisposableRegistry): IEnvironmentInfoService {
    if (envInfoService === undefined) {
        const service = new EnvironmentInfoService();
        disposables?.push({
            dispose: () => {
                service.dispose();
                envInfoService = undefined;
            },
        });
        envInfoService = service;
    }
    return envInfoService;
}
function addToQueue(
    workerPool: IWorkerPool<EQEnvInfo, InterpreterInformation | undefined>,
    env: EQEnvInfo,
    priority: EnvironmentInfoServiceQueuePriority | undefined,
) {
    return priority === EnvironmentInfoServiceQueuePriority.High
        ? workerPool.addToQueue(env, QueuePosition.Front)
        : workerPool.addToQueue(env, QueuePosition.Back);
}

async function buildEnvironmentInfoUsingCondaRun(env: EQEnvInfo): Promise<InterpreterInformation | undefined> {
    const conda = await Conda.getConda();
    const path = env.location.length ? env.location : env.executable.filename;
    const condaEnv = await conda?.getCondaEnvironment(path);
    if (!condaEnv) {
        return undefined;
    }
    const eq = await conda?.getRunEQArgs(condaEnv, true);
    if (!eq) {
        return undefined;
    }
    const interpreterInfo = await getInterpreterInfo(
        buildEQExecInfo(eq, undefined, env.executable.filename),
        CONDA_ACTIVATION_TIMEOUT,
    );
    return interpreterInfo;
}

